Home CV Portfolio
GitHub LinkedIn Rookies Twitter Itch

Water World

A World Without Glaciers

The topic I decided to choose for my Tech Demo is “A World Without Glaciers”.

This concept envisions a future where the Earth’s glaciers have completely melted due to the impacts of global warming and as a result, sea levels have risen so much that they have consumed much of the worlds landmass.

This scenario raises an important question: how would society adapt and survive in this new environment? What would daily life look like for the survivors in this world covered in water?

To explore this idea, I chose to develop a game system centred around water simulation. Therefore, creating my technical demonstration focused on recreating the physical and visual behaviours of water and oceans. This involves creating a dynamic and believable water system withing UE5, complete with realistic wave motion, buoyancy physics and environmental interactions.

My goal here is not to create a full game, but to demonstrate the technical side of the game, therefore providing the proof of concept of fluid mechanics in this world without glaciers. I will be experimenting with different types of wave simulations and comparing them to find out which will be best suited for my use case.

Project Setup

When beginning the project, I first established a clear and organized folder structure within Unreal Engine to maintain consistency and facilitate iteration throughout the development process. Proper organization is critical in technical demonstrations, especially when experimenting with multiple systems such as fluid simulation, materials, and blueprints.

To achieve this, I created a master directory named “WaterSystem”. This folder acts as the core container for all assets related to the project — including Blueprints, Materials, Meshes, Textures, and any associated Code or Data Assets. Centralizing these elements ensures that all files related to the water simulation system are easily accessible and logically grouped, avoiding confusion during testing and revisions.

Within the “WaterSystem” folder, I implemented a versioned progression layout. Each stage of development is represented by its own subfolder, named sequentially as “LV_01”, “LV_02”, and so forth.

Each “LV_N” folder contains two key subfolders:

  • Blueprints – storing all the logic-based assets such as water behaviour controllers, buoyancy test actors, and environmental interaction systems.
  • Materials – containing shaders, surface materials, and visual effects related to the appearance and movement of water.
Ocean simulation

Alongside these folders is a Level in which the blueprints are used, and the techniques are displayed.

This modular structure enables a clear development timeline, showing how each iteration builds upon the last. Additionally, each level within the sequence features an on-screen text display, providing a summary of the technical focus for that stage. For example, “Wave Simulation Prototype” or “Buoyancy and Interaction Test”. This ensures that anyone reviewing the project can quickly identify the purpose and progress of each version without having to open or analyse the underlying blueprints.

Overall, this setup not only promotes clean project organization but also supports a methodical approach to testing and evaluation, allowing me to document and compare results from different simulation methods effectively. Establishing this workflow early on was essential for maintaining clarity and control as the complexity of the water system evolved.

LV_01

I started off by creating a new basic level within Unreal Engine 5 to serve as my initial test environment for my water simulations, then removed the floor mesh provided in the level, leaving me with an empty scene that would serve as a clean workspace for my testing.

The first step in building the water system was to create a blueprint actor, which would function as the container for the water object. This blueprint was named “BP_Water_01” for clarity. Inside this blueprint I added a static mesh component, initially using UE’s default plane mesh to visualize the water’s surface.

Next, I created the first water material called “M_Water_01”, designed to handle the visual and dynamic displacement of the water surface. To construct this material, I followed guidance from GPU Gems from Nvidia, which outlines the mathematic principles behind wave simulation.

I specifically used their Sine Wave equation for a simplistic starting point in my project.

Ocean simulation Ocean simulation Ocean simulation

This was referenced from GPU Gems (Finch and Worlds, 2004, chapter 1.2):

  • Wavelength (L): the crest-to-crest distance between waves in world space. Wavelength L relates to frequency w as w = 2/L.
  • Amplitude (A): the height from the water plane to the wave crest.
  • Speed (S): the distance the crest moves forward per second. It is convenient to express speed as phase-constant phase-constant.jpg , where phase-constant.jpg = S x 2/L. CHANGE THIS LINEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
  • Direction (D ): the horizontal vector perpendicular to the wave front along which the crest travels.

Then the state of each wave as a function of horizontal position (x, y) and time (t) is defined as:

Ocean simulation

This equation was implemented using the World Position Offset within the material editor. This technique dynamically modifies the position of the water mesh vertices in real-time, producing the illusion of a moving wave across the mesh.

However, I came across an issue as soon as I started testing this out. The default plane mesh that UE uses only contains 4 vertices, which is insufficient for the method I am trying to use. As a result, when the sine wave offset is applied the surface appears flat and the edges move up and down.

To solve this, I used the modelling mode in UE to create a custom plane mesh, which I then subdivided 100. By doing this I increased the vertex count of the mesh, allowing each vertex to respond independently to the World Position Offset calculations. This change makes it so that the water mesh is actually looking like waves instead of just a moving plane mesh.

To take this further I created a material instance derived from “M_Water_01”. I did this to be able to do real-time adjustments of the wave parameters without the need to recompile the master material each time I made a change. This greatly improved the speed at which I can edit the parameters to get more realistic looking waves.

Within this instance, I exposed the parameters defined by the GPU Gems, Amplitude, Wavelength, Speed, and Direction.

LV_02

    For my next step I changed the wave generation method from Sine Waves to a more advanced Gerstner wave model. While Sine waves do implement wave movement, Gerstner waves are more effective as they apply both vertical and horizontal movement. For this I used Unreal Engines built in Gerstner Wave function to create my first more complex waves. I used this new function in a new material called “M_Water_02” to give me more control over how the waves would look, by having more parameters that can be used to specify how the waves will behave.

Ocean simulation
Ocean simulation

The function ends with making a vector 3 float from all the calculations using the parameters in the image.

Ocean simulation

I also created a material instance for this Gerstner wave water material to be able to control the waves without having to recompile.

As with the previous level, I created a corresponding material instance which enables me to adjust the wave parameters without recompiling the master material. This material instance exposes parameters such as: Amplitude, Wave Speed, Wind Direction, Wave Steepness, numWaves & Wavelength.

LV_03

For the 3rd level of development, I began working on the buoyancy system. I decided to implement this feature early in the project rather than waiting until later stages, as integrating a working buoyancy system into a complex wave simulation can quickly become challenging. This way I could ensure I had a working system that I could tweak to get working again later, instead of going through with no progress.

To keep the setup as simple as possible during my initial testing, I reverted to using Level 1’s Material with the simple Sine Wave. This allowed for me to focus on making the buoyancy functional without having to deal with additional complexity introduced by the Gerstner Waves.

    I then created a new blueprint called “BP_Buoy_01” and added a simple cube mesh to it. Next, I added the Niagara component to the cube mesh as a child.

Ocean simulation

To handle the water surface data, I implemented a Niagara Readback System. This approach utilizes Unreal Engine’s Niagara particle system to send and receive simulation data, such as the vertical displacement of the water’s surface.

Within the blueprint I created an array of Vector positions which I called “Pontoons”. Each pontoon represents a point on the object where buoyancy forces are tested and applied. These pontoons are meant to function like real buoyancy locations, as if I would attach balloons onto the side of an object.

Ocean simulation Ocean simulation Ocean simulation

After this I copied all of the Material Instances parameters and put them into variables I could use in the blueprint, this would ensure that any changes made to the Material Instance would be synced with the buoyancy blueprint.

Ocean simulation

I then used the same calculations as in the Material M_Water_01 to ensure that the water calculations, and buoyancy calculations are trying to do the same thing. This is done for each pontoon location, and the location of the pontoons are compared to the location of the water surface.

Ocean simulation

    Then I used this formula to find out how much of the volume of the Pontoon Sphere is being displaced under the water, and then I change this into force to be applied at its location. Where h is how deep under the water the pontoon is, and R is the radius of the pontoon.

Ocean simulation
Ocean simulation

This is how it ended up looking with a few other meshes added to the mix as well.

LV_04

In level 04 I moved onto integrating the buoyancy system with the more advanced Gerstner wave simulation developed in Level 02. Transitioning to Gerstner waves introduces a new layer of complexity, as these waves involve both vertical and horizontal displacement.

To begin this process, I started developing a custom Gerstner wave function as a Material Function that could be put into any material called “Water_04_Func”. This function takes in parameters and outputs the calculated world space displacement for a given position and time.

The Blueprint for the buoyancy object did not have to change as its logic was still the same, however this would have to change in the future to account for multiple Gerstner waves.

Ocean simulation Ocean simulation
LV_05

In Level 5, I wanted to shift my focus from purely physics simulations to improving the visual fidelity of the waters surface. To do this, I created a new material called “M_Water_05”, which the goal of achieving a more realistic appearance to the water. To do this I used the Pixel Depth node to implement a colour gradient that changes based on the perceived depth of the water’s surface and included some code that would affect the normals of the water with a scrolling texture.

Ocean simulation Ocean simulation

The result was a significant improvement in the visual quality of the water, with a distinct colour variation between the troughs and peaks of the water.

However, while the water was looking better, I found out that something I had not noticed in the previous iteration of the water, was that visual holes were appearing in the water mesh. This becomes more obvious depending on the number of parameters I mess around with.

Ocean simulation

    Furthermore, if I add any more Gerstner waves to the formula, the buoyancy objects no longer follow the wave surface, as they are only able to calculate based on 1 wave currently. I shall work on fixing this in the future levels.

LV_06

For Level 06 I decided to experiment with UE’s Single Layer Water shading model to further enhance the realism and visual depth of the water’s surface.

By switching the material to use Single Layer Water shading model I am able to take advantage of UE’s build in water rendering pipeline. This system is specifically designed to handle the unique optical properties of water.

To implement this, I created a new material called “M_Water_06” and set its shading model to Single Layer Water and used the Single Layer Water material node. This node gives me access to the inputs “Scattering Coefficients”, “Absorption Coefficients”, “PhaseG”, and “Colour Scale Behind Water”.

I created some vector parameters to control how the water would react to depth and colours such as:

This allows for a more realistic water shader when trying to view objects below the waters surface, especially when they get deeper under the surface.

Ocean simulation

This was done by following a tutorial by PrismaticaDev (2021)

In addition to updating the water shader model, I also revisited the wave formula to address the visual artifacts that had began appearing in earlier versions of the water system. These artifacts were visual tearing or holes in the waters surface, which I assume were caused by an incorrect calculation on the vertices by the Gerstner Wave formula I was using.

To fix this problem, I created a new custom function that followed the Gerstner Wave formula more precisely.

By doing this I was able to gain a greater understanding of how this formula functioned, and I was able to separate the X/Y/Z components into easier to understand areas.

Ocean simulation

Furthermore, I began testing using multiple Gerstner Waves added together to create more realistic moving waves.

Ocean simulation Ocean simulation

This is the end result of this levels work.

LV_07

In level 7 I made a lot of progress, so let’s get started.

I began by wanting to change how I could generate the waves, so I created a Material Parameter Collection called “MPC_Water_07”. This would allow me to have a generated set of variables that the Buoyancy actors, and the Water Materials can both pull from. The reason why I am creating this MPC is because I can address its variables on construction and randomly assign variables to it.

Ocean simulation

I do this by creating a function in the construction script of the “BP_Water_07” actor.

Ocean simulation

This function checks if the Boolean “Random Waves” is True or False. If True, it will run the “Randomize Waves” script and then will set the bool to false. This Boolean is exposed on spawn, and its default is false. By doing this I have essentially created a button on the blueprint that will randomize the waves every time its clicked, while also doing this when the editor has not even started the game yet.

The Randomize waves script accesses the parameters in the MPC and randomly assigns them a value every time the Bool is turned to true, this assigns the variables 6 times, simulating 6 different waves that will be combined. This is done for the Parameters: Amplitude, Speed, Wavelength & Direction.

The MPC is also accessed by the new “MAT_Water_07” and used by 6 Gerstner Functions to create 6 different waves, these are then combined and then used for the World Offset.

Ocean simulation

As you can now see, when I have the Water Blueprint selected, I now have access to the Boolean “Randomize waves”, and when this button is clicked, the waves change to random parameters.

Furthermore, in this level I decided to focus on the scalability and performance of the water surface by making the mesh instanced. To do so I made a function that runs after the randomize wave’s function. This function takes an input value for the desired size of the water grid and uses it as the loop length and then creates that many tiles in each direction.

Ocean simulation

In addition to changing how the water works, I also worked some more on the Buoy Blueprint Logic, as the buoyancy of the previous levels would not work with the current multiple waves.

Old Logic of Buoys with multiple Gerstner Waves:

To fix this I created a quick solution in the Buoyancy actor by converting some of the logic from previous versions into a macro.

Ocean simulation

Then I used this macro to combine 6 wave inputs where the previous logic would be for the wave height of 1 wave.

Ocean simulation

This means that the buoy actors will now follow the height of all the waves accurately.

And finally, to finish off the added logic to the Buoy Actors, I developed a way to check if the actors where completely out of the water. I did this because I use the Linear and Angular dampening to ensure that the force being applied to the actors does not fling them around too much. This code checks if all Buoyancy points are out of the water, and if they are changes the Angular and Linear dampening to 1. This means that if the actor is out of the water they will fall at a normal speed, towards the water, instead of slowly to the water.

Ocean simulation

Due to all the changes to the water, this comes with an additional benefit, which I didn’t realise till later. When I randomize the waves of the water, the buoys automatically adjust to the new water height.

LV_08

In level 8 I tweaked the water tile generation blueprint code to make it so that the water tiles would generate from the middle, instead of from the corner. In earlier versions the tiling began placement at the actor’s origin point and expanded outward along the positive axes. By modifying this I made it so that the centre of the water actor now represents the centre of the water mesh grid as well. This makes it a lot easier to align the water into areas that you would want the water, instead of trying to line it up with it expanding from a corner.

Old Logic

Ocean simulation

New Logic

Ocean simulation

Next, I moved the buoyancy logic to a custom blueprint actor component called “BPC_Buoyancy_08”. This would contain all the logic for where the pontoons are, and how they apply their force to the mesh they are applied to. In addition to this, it would make the logic modular and reusable.

By moving this functionality into a dedicated component, I achieved a more modular system design allowing any actor to be able to me made buoyant simply by attaching the BPC component without duplicating code or rebuilding the buoyancy logic from scratch.

This BPC is then applied to the “BP_Buoy_08”

Ocean simulation

This means that all the variables that are used in the Buoy actor need to be passed to the BPC component otherwise it wont know about the pontoons or the mesh.

Ocean simulation

So now all of this logic is now stored in the BPC and can be used on any actor.

Ocean simulation
LV_09

In this level I implemented a First Person Character. This was used to allow direct player interaction with the environment and to provide an intuitive way to test the new buoyancy system in real time. This approach allowed me to test each actor to ensure they would react appropriately to being given the BPC.

To achieve this, I created a First-Person Actor with the ability to fire a line trace that occurs when the player presses the “F” key. When the trace successfully hits a component that is simulating physics, a sequence of events occurs:

Ocean simulation

In this level the BP_Buoy_09 actor no longer includes the Buoyancy Component by default. Instead its just a mesh with minimal logic, containing only a color changing function when activated. This allows for testing when the First Person Actor hits it with a line trace and attaches the BPC during runtime.

Ocean simulation

Additionally, sine this is the first level where the player could move freely and observe the environment from multiple angles I made a minor but important change to the Water Material. I enabled the “Two-Sided Material” option within the Material to ensure that the water surface remains visible when viewed from below. Without this the water would appear invisible from below the waters surface.

LV_10

For level 10 I finally decided to simplify the Buoyancy wave system in the BPC

Component due to it looking like this:

Ocean simulation

Due to each wave having to be calculated on its own, it takes a long time to test and to plug in every wave. This means it’s not very modular for the number of waves.

As such I decided to create a macro where I could create a loop that would loop through the number of waves, and this is what I came up with:

Ocean simulation

This macro uses the old macro that calculates the wave height, but instead of plugging each wave in individually, it loops through them all and adds up the wave height and outputs it.

This makes the new logic look like this instead:

Ocean simulation

And this is all due to the fact of one macro here:

Ocean simulation

I also decided to add a ship model from Fab to the game as this would allow me to test how the ship would interact with the water. I did this by making a copy of BP_Buoy_10 and renaming it BP_Ship. This BP has the same Buoyancy component as the BP_Buoy and works the same way. I created some Pontoon locations on the boat and tested it in the water.

Ocean simulation Ocean simulation

After this I wanted to try implementing Multiplayer functionality to the game. This would require me to replicate the wave parameters being used to calculate the wave height and have me update the wave time manually as the base time is from each client, which would make it out of sync.

The parameters that replicated were: AmplitudeArray, SpeedArray, WaveLengthArray, DirectionArray1, DirectionArray2, DirectionArray3, WaterHeight and WaterTime.

I also had to make the variable WaterTime be run on tick to keep a proper time keeping for when new players join, this makes the water material keep the same time basis.

As you can see here though, even this is not enough. While it is close to almost working, the client is slightly behind, evident by the water coming and going in patterns on the server, before it appears on the client, or by the ship being rocking upwards on the server, while pointing down on the client.

To fix this I went into the water blueprint and enabled “Replicates” in its class defaults. This is how it looks after this minor but important change.

Taking it Further

If I had more time on this project I would have liked to have made it so the wave amount was a variable that could be changed, and have it linked between the Buoyancy Component and the Water Material, however this would have needed an overhaul of how the Water Material works, and this is not in my time allocation.

This is due to the fact that just like in the BPC, the waves in the Water Material all had to be hooked up manually and as such this approach is not very scalable or modular.

Ocean simulation

I would have also liked to have been able to make an underwater shader to make it look differently under the water, instead of being completely clear as shown when I was doing the runtime buoyancy testing with the first-person actor.

Extra Testing

Some testing that I was doing on the side that didn’t make it into the main levels was me trying to create my own editor tool, which would be used similarly to the landscape editor tool, except I would use it on my water mesh to create dips or rises in certain areas of the water.

I used the Scriptable Tools Plugin along with the Geometry Scripting plugin.

First, I started off by creating a dynamically generated mesh as this is what the Geometry Scripting plugin required to be able to edit its vertices.

Ocean simulation

I used this to test if the water material I had would play nice with the edited vertices and this is the result.

This showed me that the wireframe mesh of the dynamically generated rectangle was affecting the base offset of the wave generation, which is exactly what I wanted.

Next, I began work on creating a tool that would work like the landscape tool, this is an example of the landscape tool:

I created a few tools from the Scriptable Tools Plugin that would allow me to create my own editor tool and made a line trace when I clicked with it. This line trace would detect if there was a dynamic mesh and get the closest vertex from that mesh and offset its height upwards. However, this did not work properly and while it did get a vertex from that mesh, it was not the closest one. This is as far as I got with this testing, and this is why its not in the main levels.